<?php

namespace ServiceSdk\Libs;

/**
 * 调用restful接口
 */
class Remote
{
    // 程序内部生成header
    private static $header = [];
    // 当前请求的域名
    private static $serverName = '';

    /**
     * GET请求
     *
     * @param string $service_name         服务名.接口名
     * @param array|string $params         接口参数(不能用于GET), 如果希望数组传输为字符串 而不是post k/v方式传输,
     *                                     请在调用时使用json_encode将数组json化(因为没有name所以这种方式要求服务器使用Request:all方法接收参数)
     * @param array $query_params          用于在url中替换的参数
     * @param int $timeoutSeconds          请求超时时间（单位：秒），默认不启用超时，设定指定的秒数，则会在指定秒数后抛出超时异常
     * @param array $header                header头设置，默认是 application/json格式
     * @param string $urlReplaceMode
     *
     * @return mixed
     * @throws \Exception
     */
    public static function get($service_name, $params, array $query_params = [], int $timeoutSeconds = 0, $header = [], $urlReplaceMode = 'common')
    {
        return self::call('GET', self::url($service_name, $query_params, [], $urlReplaceMode), $params, $timeoutSeconds, $header);
    }

    /**
     * POST请求
     *
     * @param string $service_name         务服名.接口名
     * @param array|string $params         接口参数(不能用于GET), 如果希望数组传输为字符串 而不是post k/v方式传输,
     *                                     请在调用时使用json_encode将数组json化(因为没有name所以这种方式要求服务器使用Request:all方法接收参数)
     * @param array $query_params          用于在url中替换的参数
     * @param int $timeoutSeconds          请求超时时间（单位：秒），默认不启用超时，设定指定的秒数，则会在指定秒数后抛出超时异常
     * @param array $header                header头设置，默认是 application/json格式
     * @param string $urlReplaceMode       url解析规则
     *
     * @return mixed
     * @throws \Exception
     */
    public static function post($service_name, $params, array $query_params = [], int $timeoutSeconds = 0, $header = [], $urlReplaceMode = 'common')
    {
        return self::call('POST', self::url($service_name, $query_params, [], $urlReplaceMode), $params, $timeoutSeconds, $header);
    }

    /**
     * GET请求
     *
     * @param string $url                  完整的URL地址
     * @param array|string $params         接口参数(不能用于GET), 如果希望数组传输为字符串 而不是post k/v方式传输,
     *                                     请在调用时使用json_encode将数组json化(因为没有name所以这种方式要求服务器使用Request:all方法接收参数)
     * @param int $timeoutSeconds          请求超时时间（单位：秒），默认不启用超时，设定指定的秒数，则会在指定秒数后抛出超时异常
     * @param array $header                header头设置，默认是 application/json格式 可以修改
     *
     * @return mixed
     * @throws \Exception
     */
    public static function getWithUrl(string $url, $params, int $timeoutSeconds = 0, $header = [])
    {
        self::$serverName = $url;

        return self::call('GET', $url, $params, $timeoutSeconds, $header);
    }

    /**
     * POST请求
     *
     * @param string $url                  完整的URL地址
     * @param array|string $params         接口参数(不能用于GET), 如果希望数组传输为字符串 而不是post k/v方式传输,
     *                                     请在调用时使用json_encode将数组json化(因为没有name所以这种方式要求服务器使用Request:all方法接收参数)
     * @param int $timeoutSeconds          请求超时时间（单位：秒），默认不启用超时，设定指定的秒数，则会在指定秒数后抛出超时异常
     * @param array $header                header头设置，默认是 application/json格式 可以修改
     *
     * @return mixed
     * @throws \Exception
     */
    public static function postWithUrl(string $url, $params, int $timeoutSeconds = 0, $header = [])
    {
        self::$serverName = $url;

        return self::call('POST', $url, $params, $timeoutSeconds, $header);
    }

    /**
     * PUT请求
     *
     * @param string $service_name         服务名.接口名
     * @param array|string $params         接口参数(不能用于GET), 如果希望数组传输为字符串 而不是post k/v方式传输,
     *                                     请在调用时使用json_encode将数组json化(因为没有name所以这种方式要求服务器使用Request:all方法接收参数)
     * @param array $query_params          用于在url中替换的参数
     * @param int $timeoutSeconds          请求超时时间（单位：秒），默认不启用超时，设定指定的秒数，则会在指定秒数后抛出超时异常
     * @param array $header                header头设置，默认是 application/json格式
     * @param string $urlReplaceMode       url解析规则
     *
     * @return mixed
     * @throws \Exception
     */
    public static function put($service_name, $params, array $query_params = [], int $timeoutSeconds = 0, $header = [], $urlReplaceMode = 'common')
    {
        return self::call('PUT', self::url($service_name, $query_params, [], $urlReplaceMode), $params, $timeoutSeconds, $header);
    }

    /**
     * DELETE请求
     *
     * @param string $service_name         服务名.接口名
     * @param array|string $params         接口参数(不能用于GET), 如果希望数组传输为字符串 而不是post k/v方式传输,
     *                                     请在调用时使用json_encode将数组json化(因为没有name所以这种方式要求服务器使用Request:all方法接收参数)
     * @param array $query_params          用于在url中替换的参数
     * @param int $timeoutSeconds          请求超时时间（单位：秒），默认不启用超时，设定指定的秒数，则会在指定秒数后抛出超时异常
     * @param array $header                header头设置，默认是 application/json格式
     * @param string $urlReplaceMode       url解析规则
     *
     * @return mixed
     * @throws \Exception
     */
    public static function delete($service_name, $params, array $query_params = [], int $timeoutSeconds = 0, $header = [], $urlReplaceMode = 'common')
    {
        return self::call('DELETE', self::url($service_name, $query_params, [], $urlReplaceMode), $params, $timeoutSeconds, $header);
    }

    /**
     * 解析url
     *
     * @param $service_name
     *
     * @return string
     * @throws \Exception
     */
    public static function getServiceUrl($service_name)
    {
        $parse = self::parseUrl($service_name);

        return $parse['url'] . $parse['path'];
    }

    /**
     * 获取响应的请求头信息
     *
     * @param $service_name
     * @param $params
     * @param array $query_params
     * @param int $timeoutSeconds
     * @param array $header
     * @param string $urlReplaceMode
     *
     * @return mixed
     * @throws \Exception
     */
    public static function getResponseHeader($service_name, $params, array $query_params = [], int $timeoutSeconds = 0, $header = [], $urlReplaceMode = 'common')
    {
        return self::getRequestHeader('GET', self::url($service_name, $query_params, [], $urlReplaceMode), $params, $timeoutSeconds, $header);
    }

    /**
     * Description: 发送Http请求, 响应内容为空时返回http code, 否则返回json得到对应的array
     *
     * @param string $request_method GET, POST, PUT, DELETE
     * @param string $request_url
     * @param array $request_params  该参数不能作为get参数拼接到url后
     * @param int $timeoutSeconds    超时时间（单位：秒），即当请求超过指定秒数，就抛出异常，默认不检测超时
     * @param array $header
     *
     * @return mixed
     * @throws \Exception
     * @Date  : 19-3-26 下午5:11
     */
    protected static function call($request_method, $request_url, $request_params, int $timeoutSeconds = 0, $header = [])
    {
        $ch = curl_init();
        if (! is_resource($ch)) {
            throw new \Exception('无法初始化请求句柄', 50500);
        }
        if (! empty($request_params) && $request_method != 'GET') {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $request_params);
        }
        curl_setopt($ch, CURLOPT_URL, $request_url);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $request_method);
        if ($timeoutSeconds > 0) {
            // 当设置了超时时间，则开启超时模式
            curl_setopt($ch, CURLOPT_TIMEOUT, $timeoutSeconds);   //超时设置，只需要设置一个秒的数量就可以
        }
        // https请求 不验证证书和hosts
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        if (! empty($header)) {
            $header = is_string($header) ? array_merge([$header], self::$header) : array_merge($header, self::$header);
        } else {
            $header = array_merge(['content-type:application/json'], self::$header);
        }
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
        $response = trim(curl_exec($ch));
        $response_info = curl_getinfo($ch);
        $errno = curl_errno($ch);
        curl_close($ch);
        if ($errno != 0) {
            throw new \Exception("请求远程服务时发生错误($errno)", 50502);
        }
        if ($response) {
            $result = json_decode($response, true);

            return $result ? $result : $response;
        } else {
            throw new \Exception("远程服务抛出异常($errno)，http response code({$response_info['http_code']})", 50502);
        }
    }

    /**
     * 获取请求的响应头
     *
     * @param $request_method
     * @param $request_url
     * @param $request_params
     * @param int $timeoutSeconds
     * @param array $header
     *
     * @return false|string
     * @throws \Exception
     */
    protected static function getRequestHeader($request_method, $request_url, $request_params, int $timeoutSeconds = 0, $header = [])
    {
        $ch = curl_init();
        if (! is_resource($ch)) {
            throw new \Exception('无法初始化请求句柄', 50500);
        }
        if (! empty($request_params) && $request_method != 'GET') {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $request_params);
        }
        curl_setopt($ch, CURLOPT_URL, $request_url);
        curl_setopt($ch, CURLOPT_HEADER, 1); //返回response头部信息
        curl_setopt($ch, CURLINFO_HEADER_OUT, true); //TRUE 时追踪句柄的请求字符串，从 PHP 5.1.3 开始可用。这个很关键，就是允许你查看请求header
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_NOBODY, true); // 不输出正文
        if ($timeoutSeconds > 0) {
            // 当设置了超时时间，则开启超时模式
            curl_setopt($ch, CURLOPT_TIMEOUT, $timeoutSeconds);   //超时设置，只需要设置一个秒的数量就可以
        }
        // https请求 不验证证书和hosts
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        if (! empty($header)) {
            $header = is_string($header) ? array_merge([$header], self::$header) : array_merge($header, self::$header);
        } else {
            $header = array_merge(['content-type:application/json'], self::$header);
        }
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
        $response = trim(curl_exec($ch));
        $response_info = curl_getinfo($ch);
        $errno = curl_errno($ch);
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $headers = substr($response, 0, $headerSize);
        curl_close($ch);
        if ($errno != 0) {
            throw new \Exception("请求远程服务时发生错误($errno)", 50502);
        }

        return $headers;
    }

    /**
     * Description:
     *
     * @param string $service_name
     * @param array $query_params
     * @param array $excludeParamsKey  不需要绑定到url上的参数键，如果指定了键，则 上面$query_params中对应的
     *                                 键值对将不会绑定到url上，默认空，意思是全部绑定
     *
     * @param string $urlMode          参数替换类型
     *
     * @return mixed|string
     * @throws \Exception
     * @Date  : 19-3-26 下午5:11
     */
    protected static function url(string $service_name, array $query_params = [], array $excludeParamsKey = [], string $urlMode = 'common'): string
    {
        self::$serverName = str_replace('.', '_', $service_name);
        $urlArray = self::parseUrl($service_name);
        $url = $urlArray['url'] . $urlArray['path'];
        if ($urlMode === 'replace') {
            $assoc_key = false;
            if (isset($query_params['assoc']) && $query_params['assoc']) {
                $assoc_key = true;
                unset($query_params['assoc']);
            }

            foreach ($query_params as $k => $v) {
                if (is_array($v)) {
                    $uri = [];
                    foreach ($v as $kk => $item) {
                        if ($assoc_key) {
                            $uri[] = "{$k}[$kk]=$item";
                        } else {
                            $uri[] = "{$k}[]=$item";
                        }
                    }
                    $url = str_replace('{' . $k . '}', implode('&', $uri), $url);
                } else {
                    $url = str_replace('{' . $k . '}', $v, $url);
                }
            }
        } else {
            $params = self::parseQueryParams($query_params, $excludeParamsKey);
            $url = preg_match('/(\?.*)/', $url) ? $url . '&' . $params : $url . '?' . $params;
            unset($params);
        }

        return $url;
    }

    /**
     * Description: 解析连接服务
     *
     * @param string $service_name
     *
     * @return array
     * @throws \Exception
     * @Time   2020/12/26 9:59
     */
    private static function parseUrl(string $service_name)
    {
        static $service_interface = null, $service_domain = null;
        if (is_null($service_interface) || is_null($service_domain)) {
            $services = self::loadRemoteConfig();
            $service_interface = $services['interface'];
            $service_domain = $services['domain'];
        }
        list($service, $method) = explode('.', $service_name);
        if (! isset($service_domain[$service]) || empty($service_domain[$service])) {
            throw new \Exception("服务{$service}的位置未配置", 50502);
        }
        if (! isset($service_interface[$service]) || empty($service_interface[$service])) {
            throw new \Exception("服务{$service}未配置", 50502);
        } elseif (! isset($service_interface[$service][$method]) || empty($service_interface[$service][$method]) || ! is_string($service_interface[$service][$method])) {
            throw new \Exception("接口{$service_name}未定义", 50502);
        }

        return [
            'url'  => rtrim($service_domain[$service], '/') . '/',
            'path' => ltrim($service_interface[$service][$method], '/')
        ];
    }

    /**
     * Description: 解析url参数，支持无限极传参，任何形式的数组都可以，且保留原件
     *
     * @param array $params
     * @param array $excludeParamsKey 要排除的字段，暂时只支持顶级元素的字段，二级以上的字段排除暂不支持
     *
     * @return string
     * @Date  : 19-6-12 下午2:46
     */
    private static function parseQueryParams(array $params, array $excludeParamsKey): string
    {
        $return = '';
        $queryParams = [];
        if (! empty($params)) {
            foreach ($params as $key => $param) {
                if (in_array($key, $excludeParamsKey)) {
                    continue;
                }
                if (is_array($param)) {
                    $paramStr = self::parseQueryParamOfArray($key, $param);
                    $queryParams[] = rtrim($paramStr, '&');
                } else {
                    $queryParams[] = $key . '=' . $param;
                }
            }
        }
        if (! empty($queryParams)) {
            $return = implode('&', $queryParams);
        }

        return $return;
    }

    /**
     * Description: 递归解析更深层数组的参数，并组装为字符串
     *
     * @param string $key
     * @param array $params
     *
     * @return string
     * @Date  : 19-6-12 下午2:49
     */
    private static function parseQueryParamOfArray(string $key, array $params): string
    {
        $return = '';
        foreach ($params as $k => $param) {
            if (is_array($param)) {
                $key .= "[$k]";
                $return .= self::parseQueryParamOfArray($key, $param);
            } else {
                $return .= $key . "[$k]=" . $param . '&';
            }
        }

        return $return;
    }

    /**
     * Description: 解析sdk中config下配置的环境变量
     * @return array
     * @Author: Bililovy
     * @Date  : 19-6-10 下午9:28
     */
    private static function loadRemoteConfig(): array
    {
        $config = [];
        $configPath = dirname(__DIR__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'config';
        if (file_exists($configPath . DIRECTORY_SEPARATOR . 'local.php')) {
            $env = 'local';
        } else if (function_exists('env')) {
            // 如laravel框架，env函数存在
            $env = env('XIAOYAO_SDK_ENV', 'product');
        } else if (! empty($_SERVER['XIAOYAO_SDK_ENV'])) {
            $env = $_SERVER['XIAOYAO_SDK_ENV'];
        } else if (class_exists('think\facade\Env')) {
            $env = \think\facade\Env::get('XIAOYAO_SDK_ENV', 'product');
        } else {
            $env = 'product';
        }

        if (is_dir($configPath)) {
            $config = require($configPath . DIRECTORY_SEPARATOR . strtolower($env) . '.php');
        }

        return $config;
    }
}
